<?php

declare(strict_types=1);

namespace app\libs\token\driver;

use app\libs\token\Token;
use think\Cache\driver\Redis as Cache;
use think\facade\Cache as FacadeCache;

/**
 * Token的Redis实现
 * 
 * 采用一个key对应多个token的模型，每个token有自己的ttl，允许多点登录
 * 
 * 例子:
 * 
 $tokenApp = User::getTokenApp();
 
 #### 多点登录（创建token）
 $user = User::find(1);

 $token = $tokenApp->create($user['id'], null, null, $user); // 同时将user模型存入redis

 #### 单点登录（创建token）
 $user = User::find(1);

 $tokenApp->clear($user['id']);

 $tokenApp->create($user['id'], null, null, $user);

 #### 获取某个用户已经存在的token
 $user_id = 1;

 $token = $tokenApp->token($user_id);

 // 更新这个$token的有效时长

 $tokenApp->create($user_id, 2000, $token);

 #### 日常鉴权（token换取用户信息）
 $token = 'xxx';

 $user = $tokenApp->getData($token); // null 或 user模型

 if($user) $user->save(xxx);

 #### 更新token中的数据
 $user = User::find(1);

 $tokenApp->saveData($user['id'], $user);
 * 
 */
class RedisToken extends Token
{
	protected Cache $cache;

	public function __construct($config = [])
	{
		parent::__construct(array_merge([
			/** 储存key的前缀 */
			'prefix' => 'token',
			/** 储存key的分隔符，注意尽量用特殊符号，不要和全局的cache前缀等有任何重复 */
			'separator' => ':',
		], $config));

		$this->cache = FacadeCache::store('redis');
	}

	/**
	 * 扫描redis
	 * 返回命中的第一个元素的key,支持通配符* （内部使用scan，不会造成性能阻塞），如果未找到返回null
	 * @param string $pattern
	 * @param boolean $one 是否查找到第一个就返回，如果false则会返回数组
	 * @return string|array|null
	 */
	protected function searchRedis(string $pattern, $one = true)
	{
		// 要考虑全局Cache前缀
		$prefix = $this->cache->getCacheKey('');
		$scanPattern = $prefix . $pattern;

		/** @var \Redis */
		$redis = $this->cache->handler();
		$redis->setOption(\Redis::OPT_SCAN, \Redis::SCAN_RETRY); //必须设置，扫描不到自动迭代尝试下一个，否则limit->1可能失败

		// 扫描
		$cursor = null;
		$results = [];
		while ($cursor !== 0) {
			$result = $redis->scan($cursor, $scanPattern, 1);
			if ($one) {
				if (!empty($result)) {
					return substr($result[0], strlen($prefix));
				}
			} else {
				foreach ($result as $r) {
					$results[] = substr($r, strlen($prefix));
				}
			}
		}

		return $one ? null : $results;
	}

	/**
	 * 获取token的储存key
	 * @param string $key 数据的唯一标识（可以传入*通配）
	 * @param string $token token（可以传入*通配）
	 */
	protected function tokenSaveName($key, $token): string
	{
		$sep = $this->config['separator'];
		return $this->config['prefix'] . $sep .
			$this->config['name'] . $sep .
			$key . $sep .
			$token;
	}

	// 获取数据的储存key
	protected function dataSaveName($key): string
	{
		$sep = $this->config['separator'];
		return  $this->config['prefix'] . $sep .
			$this->config['name'] . $sep .
			'key' . $sep .
			$key;
	}

	/**
	 * 从一个完整的token名中获取其中的数据唯一标识
	 * 本函数要和 tokenSaveName() 中的保存方法保持一致
	 * @param string $tokenName token储存在redis中的完整名称
	 * @return string
	 */
	protected function parseKey(string $tokenName): string
	{
		$exps = explode($this->config['separator'], $tokenName);
		return $exps[2];
	}


	/**
	 * 从一个完整的token名中获取其中的token
	 * 本函数要和 tokenSaveName() 中的保存方法保持一致
	 * @param string $tokenName token储存在redis中的完整名称
	 * @return string
	 */
	protected function parseToken(string $tokenName): string
	{
		$exps = explode($this->config['separator'], $tokenName);
		return $exps[3];
	}

	/**
	 * 通过token直接获取缓存的数据
	 * @param string $token
	 * @return mixed
	 */
	public function getData(string $token)
	{
		$tokenName = $this->searchRedis($this->tokenSaveName('*', $token));
		if (!$tokenName) return null;

		// 从$tokenName中截取出key
		$dataKey = $this->parseKey($tokenName);
		$result = $this->cache->get($this->dataSaveName($dataKey));

		return $result;
	}

	/**
	 * 保存数据到redis
	 * @param string|int $key 数据唯一标识
	 * @param mixed $data 数据的值
	 * @return boolean
	 */
	public function saveData($key, $data)
	{
		return $this->cache->set($this->dataSaveName($key), $data);
	}

	/**
	 * ---- 以下实现Token接口 ----
	 */
	public function get(string $token)
	{
		$tokenName = $this->searchRedis($this->tokenSaveName('*', $token));
		if (!$tokenName) return null;

		return $this->parseKey($tokenName);
	}

	public function token($key, $all = false)
	{
		$tokenName = $this->searchRedis($this->tokenSaveName($key, '*'), !$all);
		if (!$all) return $tokenName ? $this->parseToken($tokenName) : null;
		else return array_map(fn ($it) => $this->parseToken($it), $tokenName);
	}

	public function create($key, $expireTime = null, string $token = null, $data = null)
	{
		if (is_null($token)) $token = $this->generateToken();
		if (is_null($expireTime)) $expireTime = $this->config['expire_time'];

		// 保存数据
		if (!is_null($data)) $this->saveData($key, $data);

		// 保存token
		$this->cache->set($this->tokenSaveName($key, $token), null, $expireTime == -1 ? null : $expireTime);

		return $token;
	}

	public function has(string $token)
	{
		return !!$this->searchRedis($this->tokenSaveName('*', $token));
	}

	public function delete(string $token)
	{
		$tokenName = $this->searchRedis($this->tokenSaveName('*', $token));
		if (!$tokenName) return false;

		return $this->cache->delete($tokenName);
	}

	public function clear($key = null)
	{
		// 清除所有的数据储存项
		$dataNames = $this->searchRedis($this->dataSaveName(is_null($key) ? '*' : $key), false);
		foreach ($dataNames as $name) {
			$this->cache->delete($name);
		}

		// 清除所有token项
		$tokenNames = $this->searchRedis($this->tokenSaveName(is_null($key) ? '*' : $key, '*'), false);
		foreach ($tokenNames as $name) {
			$this->cache->delete($name);
		}
		return true;
	}

	public function ttl($token, $expireTime = null)
	{
		$tokenName = $this->searchRedis($this->tokenSaveName('*', $token));
		if (!$tokenName) return false;

		if (is_null($expireTime)) {
			return $this->cache->ttl(
				$this->cache->getCacheKey($tokenName)
			);
		} else {
			return $this->cache->expire(
				$this->cache->getCacheKey($tokenName),
				$expireTime
			);
		}
	}
}
